package baseFx.common.context.spring;

import baseFx.common.Action1T;
import baseFx.common.Stream;
import baseFx.common.context.BeanDef;
import baseFx.common.context.BeanLife;
import baseFx.common.utils.ClassUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultSingletonBeanRegistry;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor;
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
import org.springframework.context.annotation.Scope;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.GenericApplicationContext;

import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

public class SpringContextExtend {
    private GenericApplicationContext context;
    private ContextScope contextScope = new ContextScope();
    private Map<Class<?>, Object> externalBean = new ConcurrentHashMap<>(8);
    private List<Object> prototypes = new ArrayList<>();
    private Map<String, GenericApplicationContext> childes = new ConcurrentHashMap<>(8);
    private Map<String, List<Action1T>> beanInitAfter = new HashMap<>();
    private List<DefaultScopeSetting> defaultScopeSettings = new ArrayList<>();

    public SpringContextExtend(GenericApplicationContext context) {
        this.context = context;
        context.getBeanFactory().registerScope(BeanScopes.SCOPE_CONTEXT, contextScope);
        registerEvents();
    }

    public void addChild(GenericApplicationContext context) {
        childes.put(context.getId(), context);
        context.addApplicationListener((ApplicationListener<ContextClosedEvent>) e -> childes.remove(e.getApplicationContext().getId()));
    }

    public void setDefaultScope(String beanName, BeanDefinition def) {
        if (def.getRole() >= 2 || defaultScopeSettings.isEmpty()) {
            return;
        }
        Class beanClass;
        if (def.getBeanClassName() != null) {
            beanClass = ClassUtils.getClassForName(def.getBeanClassName());
            if (beanClass == null) {
                return;
            }
            if (isConfiguration(def)) {
                return;
            }
            if (ClassUtils.isAnnotationPresent(beanClass, Scope.class)) {
                return;
            }
        } else if (def.getFactoryBeanName() != null && def.getFactoryMethodName() != null) {
            if ("".equals(def.getScope()) == false) {
                return;
            }
            if (context.containsBean(def.getFactoryBeanName()) == false) {
                return;
            }
            BeanDefinition mDef = context.getBeanDefinition(def.getFactoryBeanName());
            if (mDef.getBeanClassName() == null) {
                return;
            }
            Class clazz = ClassUtils.getClassForName(mDef.getBeanClassName());
            if (clazz == null || def.getFactoryMethodName() == null) {
                return;
            }
            Method method = ClassUtils.getMethod(clazz, def.getFactoryMethodName());
            if (method == null) {
                return;
            }
            beanClass = method.getReturnType();
        } else {
            return;
        }
        if (beanClass == null || beanClass == Void.class) {
            return;
        }
        DefaultScopeSetting setting = defaultScopeMatches(beanClass.getCanonicalName());
        if (setting != null) {
            def.setScope(setting.getScope());
        }
    }

    private void setDefaultScope() throws BeansException {
        for (String name : context.getBeanDefinitionNames()) {
            BeanDefinition def = context.getBeanDefinition(name);
            setDefaultScope(name, def);
        }
    }

    private void registerEvents() {
        context.addApplicationListener((ApplicationListener<ContextClosedEvent>) e -> {
            if (e.getApplicationContext() == context) {
                contextOnClosing();
            }
        });
        context.addBeanFactoryPostProcessor(this::registerBeanPostProcessor);
    }

    private void registerExternalBean(String name, Class<?> beanClass, Object bean) {
        externalBean.put(beanClass, bean);
        context.registerBean(name, beanClass, (Supplier) () -> bean, bd -> {
            bd.setScope(BeanScopes.SCOPE_SINGLETON);
            bd.setPrimary(true);
        });
    }

    private void registerBeans() throws BeansException {
        if (context.getParent() instanceof GenericApplicationContext == false) {
            return;
        }
        GenericApplicationContext parent = (GenericApplicationContext) context.getParent();
        List<String> pass = new ArrayList<>();
        for (Class<?> cls : externalBean.keySet()) {
            String[] names = parent.getBeanNamesForType(cls);
            pass.addAll(Arrays.asList(names));
        }
        List<String> names = Arrays.asList(context.getBeanDefinitionNames());
        for (String name : parent.getBeanDefinitionNames()) {
            BeanDefinition bd = parent.getBeanFactory().getBeanDefinition(name);
            if (pass.contains(name) || names.contains(name)) {
                continue;
            }
            if (BeanScopes.SCOPE_CONTEXT.equals(bd.getScope())) {
                context.registerBeanDefinition(name, bd);
            } else if (BeanScopes.SCOPE_PROTOTYPE.equals(bd.getScope())) {
                //显式注册才能触发此context的prototype创建事件，否则只会触发父级的事件
                context.registerBeanDefinition(name, bd);
            }
        }
    }

    private void registerBeanPostProcessor(ConfigurableListableBeanFactory factory) {
        context.registerBean(ConfigurationClassPostProcessor.class);
        context.registerBean(CommonAnnotationBeanPostProcessor.class, (Supplier) FxCommonAnnotationBeanPostProcessor::new);
        context.registerBean(AutowiredAnnotationBeanPostProcessor.class, (Supplier) FxAutowiredAnnotationBeanPostProcessor::new);

        factory.addBeanPostProcessor(new BeanPostProcessor() {
            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
                collectDisposablePrototype(factory, bean, beanName);
                invokeOnCreate(beanName, bean);
                return bean;
            }
        });
        if (defaultScopeSettings.isEmpty() == false) {
            setDefaultScope();
        }
        registerBeans();
    }

    public void registerDefaultScope(DefaultScopeSetting setting) {
        defaultScopeSettings.add(setting);
    }

    private boolean isConfiguration(BeanDefinition def) {
        int count = 0;
        for (String item : def.attributeNames()) {
            if (item.endsWith("configurationClass") || item.endsWith("preserveTargetClass")) {
                count++;
            }
        }
        return count == 2;
    }

    private DefaultScopeSetting defaultScopeMatches(String className) {
        List<DefaultScopeSetting> list = new ArrayList<>();
        for (DefaultScopeSetting item : defaultScopeSettings) {
            if (item.matches(className)) {
                list.add(item);
            }
        }
        if (list.size() == 0) {
            return null;
        } else if (list.size() == 1) {
            return list.get(0);
        } else {
            return Stream.create(list).max(i -> i.getTarget().length());
        }
    }

    private void invokeOnCreate(String beanName, Object bean) {
        if (beanInitAfter.containsKey(beanName)) {
            for (Action1T action : beanInitAfter.get(beanName)) {
                action.execute(bean);
            }
        }
    }

    private void collectDisposablePrototype(ConfigurableListableBeanFactory factory, Object bean, String beanName) {
        if (bean == context) {
            return;
        }
        if (context.isBeanNameInUse(beanName) == false) {
            return;
        }
        if (factory.isPrototype(beanName) == false) {
            return;
        }
        if (externalBean.containsValue(bean)) {
            return;
        }
        if (bean instanceof AutoCloseable || bean instanceof DisposableBean) {
            prototypes.add(bean);
            return;
        }
        BeanDefinition def = factory.getBeanDefinition(beanName);
        if (def.getDestroyMethodName() != null) {
            prototypes.add(bean);
        }
    }

    private void disposablePrototype() {
        for (Object o : Stream.create(prototypes).reverse().toList()) {
            context.getBeanFactory().destroyBean(o);
            if (o instanceof AutoCloseable) {
                try {
                    ((AutoCloseable) o).close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        prototypes.clear();
    }

    private void closeChildContext() {
        Stream.create(childes.values()).reverse().foreach(AbstractApplicationContext::close);
        childes.clear();
    }

    private void passExternalBeanOnDisposable() {
        if (externalBean.size() > 0 && context.getBeanFactory() instanceof DefaultSingletonBeanRegistry) {
            DefaultSingletonBeanRegistry registry = (DefaultSingletonBeanRegistry) context.getBeanFactory();
            String[] names = registry.getSingletonNames();
            for (String name : names) {
                Object bean = registry.getSingleton(name);
                if (externalBean.containsValue(bean)) {
                    registry.registerDisposableBean(name, null);
                }
            }
            externalBean.clear();
        }
    }

    public void setRegisterParam(BeanDef... defs) {
        for (BeanDef param : defs) {
            Class<?> beanClass;
            if (param.beanType != null) {
                beanClass = param.beanType;
            } else if (param.bean != null) {
                beanClass = param.bean.getClass();
            } else {
                throw new RuntimeException("缺少beanClass");
            }
            if (param.bean != null) {
                registerExternalBean(param.name, beanClass, param.bean);
            } else {
                String[] tmp = null;
                boolean isPrimary = false;
                if (context.getParent() instanceof ConfigurableApplicationContext) {
                    ConfigurableApplicationContext ptx = (ConfigurableApplicationContext) context.getParent();
                    tmp = ptx.getBeanNamesForType(beanClass);
                    if (tmp != null) {
                        for (String item : tmp) {
                            BeanDefinition def = ptx.getBeanFactory().getBeanDefinition(item);
                            if (def.isPrimary()) {
                                isPrimary = true;
                                break;
                            }
                        }
                    }
                }
                if (isPrimary) {
                    continue;
                }
                String[] names = tmp;
                context.registerBean(param.name, beanClass, def -> {
                    if (param.life == null || param.life == BeanLife.context) {
                        def.setScope(BeanScopes.SCOPE_CONTEXT);
                    } else if (param.life == BeanLife.prototype) {
                        def.setScope(BeanScopes.SCOPE_PROTOTYPE);
                    } else if (param.life == BeanLife.singleton) {
                        def.setScope(BeanScopes.SCOPE_SINGLETON);
                    }
                    if (names != null && names.length > 0) {
                        def.setPrimary(true);
                    }
                    if (param.onCreate != null) {
                        if (beanInitAfter.containsKey(def.getBeanClassName()) == false) {
                            beanInitAfter.put(def.getBeanClassName(), new ArrayList<>());
                        }
                        beanInitAfter.get(def.getBeanClassName()).add(param.onCreate);
                    }
                });
            }
        }
    }

    private void contextOnClosing() {
        closeChildContext();
        beanInitAfter.clear();
        passExternalBeanOnDisposable();
        disposablePrototype();
        contextScope.destroy();
        defaultScopeSettings.clear();
        childes = null;
        externalBean = null;
        prototypes = null;
        contextScope = null;
        beanInitAfter = null;
        defaultScopeSettings = null;
    }

    private class FxCommonAnnotationBeanPostProcessor extends CommonAnnotationBeanPostProcessor {
        @Override
        public boolean requiresDestruction(Object bean) {
            if (SpringContextExtend.this.externalBean.containsValue(bean) == false) {
                return super.requiresDestruction(bean);
            }
            return false;
        }

        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            if (SpringContextExtend.this.externalBean.containsValue(bean) == false) {
                return super.postProcessBeforeInitialization(bean, beanName);
            }
            return bean;
        }
    }

    private class FxAutowiredAnnotationBeanPostProcessor extends AutowiredAnnotationBeanPostProcessor {
        @Override
        public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
            if (SpringContextExtend.this.externalBean.containsValue(bean) == false) {
                return super.postProcessProperties(pvs, bean, beanName);
            }
            return pvs;
        }
    }
}
